🏦 先想象一个场景:
你和你妈共用一张银行卡,里面有 1000 元。
同一秒钟,你在便利店刷卡买了 100 元零食,同时 你妈在另一家店刷了 200 元菜钱。
👉 账户里 应该 剩多少?很简单,700 元。
但如果数据库没处理好,结果可能是 800、900,甚至还是 1000……
本章就是要解决这个问题:多人同时操作数据库时,怎样保证算得对?
一、为什么需要"事务"?重点
1.1 数据库为什么要支持"并发"?
数据库的最大特点就是 数据共享—— 一份数据,很多人都要用。
你查的时候,别人必须等。等你查完了,下一个人才能用。
❌ 慢,CPU 大部分时间在闲着
多个人同时操作数据库,效率高。
✅ 快,但是要解决"同时操作会不会出错"
并发 = 多人同时用 = 提高效率,但需要 "控制" 避免出错。
1.2 什么是事务(Transaction)?必考
事务是 一组数据库操作的集合,这一组操作必须 "要么全部成功,要么全部失败",不允许只成功一半。
张三给李四转 100 元,数据库里实际要做 两件事:
① 张三账户:余额 - 100
② 李四账户:余额 + 100
这两步必须 "绑在一起"。如果第①步成功了、第②步失败了 —— 那 100 块就凭空消失了!
所以我们要把这两步做成 一个事务:要么两步都成功,要么两步都不做(一旦中间出问题,刚才的扣款也撤销)。
MySQL 中的事务三件套必考
START TRANSACTION; -- ① 启动事务
-- 这中间写若干条 SQL ……
COMMIT; -- ② 提交:所有操作正式生效
ROLLBACK; -- ③ 回滚:撤销所有操作,回到事务开始之前
COMMIT = 我做完了,请保存 ✅
ROLLBACK = 我中间出错了,请假装我没做过 ❌
1.3 事务的 ACID 四大性质必考
事务能"靠谱"地工作,是因为它必须满足 4 条性质,合起来叫 ACID。这是必考点,每个字母代表什么、什么意思,必须背下来。
事务里的操作,要么全部做完,要么全部不做,不能做一半。
事务执行 前后,数据库都处于"合理"的状态(钱不能凭空多也不能凭空少)。
多个事务同时跑,互相不能看到对方的中间状态,就像各自独占数据库一样。
事务一旦提交,数据 永远不会丢。哪怕数据库立刻断电,重启后数据还在。
用转账例子讲透 ACID(这就是考点)
张三向李四转 100 元,假设有个事务:
START TRANSACTION;
UPDATE account SET R = R - 100 WHERE id = '张三';
UPDATE account SET R = R + 100 WHERE id = '李四';
COMMIT;
A 原子性:两条 UPDATE 必须 都成功。如果第二条失败,要 ROLLBACK 撤销第一条,否则张三扣了钱、李四没收到 —— 钱凭空消失。
C 一致性:转账前后,张三 + 李四的总金额 不变。钱不能凭空出现,也不能凭空消失。
I 隔离性:如果同一时刻还有别的转账在跑,它们不能看到这个事务"扣了钱但还没加给李四"的中间状态。
D 持久性:一旦 COMMIT,张三 -100、李四 +100 就 写入磁盘,哪怕下一秒服务器宕机,重启后数据还在。
本章后面所有内容,本质上都是为了维护 ACID 中的"I(隔离性)"。
因为多个事务并发执行,最容易出问题的就是隔离性 —— 怎么让事务感觉不到彼此存在?这就要靠 隔离级别 和 封锁机制。
二、并发会带来什么问题?4 种"翻车"场景核心重点
当多个事务一起跑、隔离性没做好时,会出现 4 种典型问题。必须背得滚瓜烂熟,因为这是考试核心。
2.1 问题一:丢失更新(Lost Update)
两个事务同时改 同一条数据,后写的 把 先写的 给覆盖了 —— 先写的修改"凭空消失"。
你和你妈同时打开 Word 编辑同一份文件 —— 你改了第一段保存,她 改了第二段保存。结果:她保存的版本里没有你改的第一段!
因为她保存时用的是"打开时的旧版本"覆盖掉了你的修改。
时序演示
账户初始余额 1000 元。事务 T1 想取 100,事务 T2 想取 200,正确结果应该是 700。
| 时间 | 事务 T1 | 余额 R | 事务 T2 |
|---|---|---|---|
| t1 | SELECT R = 1000 | 1000 | — |
| t2 | — | 1000 | SELECT R = 1000 |
| t3 | UPDATE R = 1000-100 = 900 | 900 | — |
| t4 | — | 800 ❌ | UPDATE R = 1000-200 = 800 |
T2 在 t2 时刻拿到的是 1000(旧值),它不知道 T1 已经改成 900 了。等 t4 写回 800 时,T1 的扣款 100 元就丢了。
2.2 问题二:脏读(Dirty Read)
一个事务读到了 另一个事务还没提交 的数据。如果对方后来 回滚 了,那读到的就是"假数据"。
同事跟你说:"老板刚说要给我们涨工资 5000!" 你高兴地告诉了爸妈。
结果第二天老板说:"昨天那是开玩笑的。" —— 你已经传出去的消息变成了"假新闻"。
同事的"涨工资"还没正式生效(没 COMMIT),就被你看到了 —— 这就是脏读。
时序演示
| 时间 | 事务 T1 | 余额 R | 事务 T2 |
|---|---|---|---|
| t1 | SELECT R = 1000 | 1000 | — |
| t2 | UPDATE R = 900(未提交) | 900 | — |
| t3 | — | 900 | SELECT R = 900 ⚠️ 读到了未提交的 |
| t4 | ROLLBACK(撤销!) | 1000 | — |
| t5 | — | 1000 | 但 T2 还在用 R = 900 ❌ |
读到的数据来自 "还没提交" 的事务(甚至最后回滚了)—— 这就叫"脏"。
2.3 问题三:不可重复读(Unrepeatable Read)
同一个事务里,读了两次同一行数据,结果 不一样 —— 因为中间另一个事务把它改了 并且提交了。
你早上看天气预报:"今天 25℃",决定不带外套出门。
中午再看:"今天 15℃" ——天气预报中途被气象局更新了。
同一件事(今天的天气)你看了两次,结果不一样。
时序演示
| 时间 | 事务 T1 | 余额 R | 事务 T2 |
|---|---|---|---|
| t1 | SELECT R = 1000(第一次读) | 1000 | — |
| t2 | — | 1000 | UPDATE R = 800 |
| t3 | — | 800 | COMMIT(已提交!) |
| t4 | SELECT R = 800(第二次读 ≠ 1000 ❌) | 800 | — |
这次 T2 是 已经提交 的(注意 t3 那个 COMMIT),所以 T1 读到的 800 是 真实数据,不是脏数据。
但问题在于:T1 在同一个事务里 读两次结果不一样 —— 这就是"不可重复读"。
2.4 问题四:幻象读(Phantom Read)
同一个事务里,用相同条件查两次,得到的 记录条数 不一样 —— 因为另一个事务 插入或删除 了符合条件的行。
你在班级群里数"今天来上课的人":第一次数 30 人。
转头又数一遍:变成 32 人 —— 因为有 2 个人刚才偷偷加群了。
条件没变(都是数群里的人),但 "人数" 变了,仿佛凭空多出来 2 个"幻影"。
时序演示
| 时间 | 事务 T1 | 记录数 | 事务 T2 |
|---|---|---|---|
| t1 | SELECT * WHERE R<1000 → 3 行 | 3 | — |
| t2 | — | 3 | INSERT 一条 R=800 的新记录 |
| t3 | — | 4 | COMMIT |
| t4 | SELECT * WHERE R<1000 → 4 行 ❌ | 4 | — |
"不可重复读" vs "幻象读" —— 学生最容易混淆,老师最爱考这里!
👉 不可重复读:同一行的 值变了(比如 1000 → 800)
👉 幻象读:查询条件下的 行数变了(比如 3 行 → 4 行)
一句话:一个改了"内容",一个改了"数量"。
2.5 4 种问题速记口诀必考
- 丢失更新 = "覆盖"(两人改一份,后写的覆盖先写的)
- 脏读 = "偷读"(读到了别人没提交的中间数据)
- 不可重复读 = "同行变值"(同一行的值,第二次读变了)
- 幻象读 = "行数变化"(同一条件,行数变多/变少了)
某事务对学生表 WHERE 班级='1班' 查询了两次,第一次返回 30 行,第二次返回 32 行(中间有人新增了 2 个 1 班学生 并提交了)。这属于哪种问题?
✅ 正确答案:D · 幻象读
判断思路:① 已提交 → 排除"脏读";② 行数变了(不是值变了) → 是"幻象读",不是"不可重复读"。
三、解决之道(一):隔离级别核心重点
3.1 4 种隔离级别(从低到高)必考
SQL 标准定义了 4 种隔离级别,它们能挡住的问题不一样。级别越高 → 越安全 → 但也越慢。
没有"最好"的级别,只有"最适合业务"的级别。
MySQL 默认 REPEATABLE READ(可重复读)—— 平衡安全和性能,大部分场景够用。
3.2 一张对照表打通全部考点必考
下面这张表 必须背下来,考试 100% 会出现。√ 表示"能避免该问题",✗ 表示"挡不住"。
| 隔离级别 | 脏读 | 不可重复读 | 幻象读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✗ | ✗ | ✗ |
| READ COMMITTED(读已提交) | √ | ✗ | ✗ |
| REPEATABLE READ(可重复读)⭐ 默认 | √ | √ | ✗ |
| SERIALIZABLE(串行化) | √ | √ | √ |
"未提交什么都不挡 → 已提交挡脏读 → 可重复读再挡不可重复读 → 串行化全挡"
每升一级,多挡一种问题。从低到高刚好对应:脏读 → 不可重复读 → 幻象读。
设置隔离级别(语法,了解即可)了解
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别(4 选 1)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
💡 SESSION 表示只对当前会话生效;GLOBAL 表示对所有连接生效。下节实验课会动手操作。
四、解决之道(二):封锁机制重点
4.1 什么是封锁(Locking)?
事务在操作数据前,先给数据 "加锁"。其他事务想动这份数据,必须 等锁释放。
公共自习室的座位。你想坐 → 先把书包放上去("加锁")→ 别人就不能来坐 → 你走的时候把书包拿走("释放锁")→ 别人才能用。
MySQL 提供 两种封锁粒度(锁住多大一片):
锁住 整张表。
✅ 简单,开销小
❌ 并发度低(一锁整桌都不能用)
只锁住 用到的那几行。
✅ 并发度高
❌ 开销大,可能死锁
💡 MySQL 的 InnoDB 引擎默认用 行级锁,性能好。
4.2 两种锁类型:X 锁 与 S 锁必考
规则:加了 X 锁,只有自己能读和写,其他人连读都不行。
✋ 关键词:独占,谁也别碰
规则:加了 S 锁,大家都能读,但谁都不能写(包括自己)。
🤝 关键词:共享读,但都不能写
锁兼容矩阵:能不能同时加?必考
已经有一种锁的情况下,能不能再加另一种?
"只有 S+S 才能并存" —— 只要涉及 X 锁,必冲突。
本质:读不冲突;写排斥一切(既排斥别人写,也排斥别人读)。
MySQL 加锁语法了解
LOCK TABLES account READ; -- 给 account 表加 S 锁(读锁)
LOCK TABLES account WRITE; -- 给 account 表加 X 锁(写锁)
UNLOCK TABLES; -- 释放当前会话所有锁
4.3 封锁协议(三级递进)了解
"在什么时候加什么锁、什么时候释放" —— 这种约定就叫 封锁协议。教材分了三级,递进的。不需要背时序,只需要记住下面这张对照表:
| 封锁协议 | 规则要点 | 丢失更新 | 脏读 | 不可重复读 |
|---|---|---|---|---|
| 一级封锁协议 | 写之前加 X 锁,事务结束才释放 | √ | ✗ | ✗ |
| 二级封锁协议 | 在一级基础上:读之前加 S 锁,读完立刻释放 | √ | √ | ✗ |
| 三级封锁协议 | 在一级基础上:读之前加 S 锁,事务结束才释放 | √ | √ | √ |
关键看 S 锁什么时候释放:
① 一级:根本没 S 锁(只管写) → 防 丢失更新
② 二级:S 锁 读完即释 → 多防 脏读(持锁期间别人不能写)
③ 三级:S 锁 事务结束才释 → 多防 不可重复读(整个事务期间别人都不能改)
五、死锁了解即可
5.1 什么是死锁?
两个或多个事务 互相等对方放锁,结果谁也动不了 —— 系统卡死。
两个人过独木桥,各从一头走到中间,都不肯让。
A 等 B 退回去,B 等 A 退回去 —— 永远卡在那。
典型死锁场景
| 时间 | 事务 T1 | 事务 T2 |
|---|---|---|
| t1 | 锁定 学生表 ✅ | — |
| t2 | — | 锁定 成绩表 ✅ |
| t3 | 想锁 成绩表 → 等待 ⌛ | — |
| t4 | — | 想锁 学生表 → 等待 ⌛ |
| t5 | 永远等下去 💀 | 永远等下去 💀 |
T1 拿着学生表的锁,想要成绩表的锁;T2 拿着成绩表的锁,想要学生表的锁 —— 互相等,谁都不让。
5.2 怎么避免死锁?了解
① 顺序加锁法:所有事务 按相同顺序 锁表。比如规定都先锁学生表、再锁成绩表 —— 就不会出现"互等"。
② 一次加锁法:把需要的锁 一次性全申请,要么全拿到要么都不拿。
③ 直接申请够大的锁:要更新就直接 X 锁,不要"先 S 锁后升级 X 锁" —— 升级过程容易死锁。
MySQL 自己会 自动检测死锁,发现后会主动回滚一个事务,让另一个继续执行。所以你写代码时,主要做好 预防(按顺序加锁)就行。
六、本章小结
📋 三句话总结整章
- 数据库要 并发(多人同时用)才能性能好 → 但并发会带来 4 种数据不一致问题。
- 事务 ACID 是目标;隔离级别 是策略(4 个级别防不同问题);封锁 是底层手段。
- 选哪个级别、用哪种锁,本质都是在 "安全 ↔ 速度" 之间做权衡。
本章必考点回顾
⭐ 期末考点(按出现频率排)
- 4 种不一致性问题:识别 + 区分(重中之重,不可重复读 vs 幻象读 必考)
- 4 种隔离级别 vs 3 种问题的对照表(必背)
- 事务的 ACID 四大性质(简答题常考)
- X 锁、S 锁的区别 + 兼容矩阵
- 事务三件套:START TRANSACTION / COMMIT / ROLLBACK
课堂综合测验
事务 T1 修改了余额但还没提交,T2 读到了 T1 修改后的值。后来 T1 因故障回滚。这是?
✅ 正确:B。读到了"未提交"+"对方还回滚了" = 脏读的标志。
能避免"不可重复读"的最低隔离级别是?
✅ 正确:C。回头看那张对照表 —— READ COMMITTED 还挡不住不可重复读,REPEATABLE READ 才行(也是 MySQL 默认)。
T1 已经对表 R 加了 S 锁。下面哪个操作能 立刻成功?
✅ 正确:C。S+S 是唯一兼容的组合(多人共读)。A、B 都涉及写,会被 S 锁挡住;D 中 T1 自己持 S 锁也不能写(S 锁只让读)。
🚀 下节课:上机实验
下节实验课,我们会实际打开两个 MySQL 窗口,亲手做以下事情:
- 🔬 在不同隔离级别下,亲眼看到脏读、不可重复读是怎么发生的
- 🔒 用
LOCK TABLES制造阻塞场景 - 📝 大量练习题,巩固 4 种问题的辨认
请提前确保 MySQL 装好,能正常登录。